package com.sky.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.*;
import com.sky.entity.*;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.OrderBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.*;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.utils.HttpClientUtil;
import com.sky.utils.WeChatPayUtil;
import com.sky.vo.OrderPaymentVO;
import com.sky.vo.OrderStatisticsVO;
import com.sky.vo.OrderSubmitVO;
import com.sky.vo.OrderVO;
import com.sky.websocket.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import java.beans.beancontext.BeanContext;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author DragonflyYue
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private WeChatPayUtil weChatPayUtil;   //微信支付相关
    @Autowired    //下面这个就是用来实现,当前后端向前端商家推送消息的,,即用户下单完成,,提示:  待接单的弹窗给商家
    private WebSocketServer webSocketServer;

    //下面的两个设置在application.xml配置文件中配置了,是百度地图平台对应的一些配置
    @Value("${sky.shop.address}")
    private String shopAddress;

    @Value("${sky.baidu.ak}")
    private String ak;
    
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @Transactional
    @Override
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
        
        //1.处理各种业务异常情况
            //比如用户购物车为空,就下单,那肯定不行;还有配送地址为空,这也不能让他下单成功
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null){
            //抛出业务异常
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        
        //检查用户的收获地址是否超出配送范围 (自己没有公网ip,所以这段代码就注释掉了;用到直接取消注释就可以)
        checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() 
                + addressBook.getDetail());
            //这个方法checkOutOfRange在最下面737行左右,有定义,,我直接拷贝的老师的代码,老师也没讲;  
                //是百度地图开放平台提供的接口
        
        //查询当前用户的购物车数据
        Long userId = BaseContext.getCurrentId();
            //下面方法要求传入的是shoppingCart对象,,所以还需要单独封装
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);
        
            //用下面这种方式也可以,,我就是用的下面这个,因为属性就一个,所以可以用上面这种传统的方式赋值
        //ShoppingCart shoppingCart = ShoppingCart.builder()
        //        .userId(userId)
        //        .build();
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.size() == 0){
            //说明购物车是空的,你还想下单,,给你个大逼斗,让你冷静冷静
            //抛出业务异常
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }
        
        //补充说明: 老师说上面异常的处理,其实也可以不处理,一般情况下,也不会发生;但这里老师为了代码的健壮性
            //而且前端小程序也会判断,如果购物车为空,肯定也是不能提交的

        //2.向订单表插入1条数据
        Orders orders = new Orders();
            //这个类里面的属性也太多了,这要是一个个赋值得到猴年马月
        //对象属性拷贝
        BeanUtils.copyProperties(ordersSubmitDTO,orders);
        orders.setPhone(addressBook.getPhone());
        orders.setConsignee(addressBook.getConsignee());   //收货人
        orders.setNumber(String.valueOf(System.currentTimeMillis()));  //使用当前时间戳,作为订单号
        orders.setUserId(userId);   //当前用户的用户id
        orders.setStatus(Orders.PENDING_PAYMENT);   //订单状态,代付款状态
        orders.setPayStatus(Orders.UN_PAID);  //默认是未支付的状态
        orders.setOrderTime(LocalDateTime.now());
            // 下面的地址,是自己忘了没放入,人家老师代码是没问题的,所以后面我查订单信息的时候,详细地址为null,
                //还好老子聪明,直到怎么查找,是不是有问题,我可太厉害了叭;就是太粗心了
        String adress = addressBook.getProvinceName() + addressBook.getDistrictName() 
                + addressBook.getDetail();
        orders.setAddress(adress);
        
        orderMapper.insert(orders);
        //上面这里要返回对应的orders表中的id,因为下面的订单明细表,要用到,,第102行就用到了
        
        //3.向订单明细表插入n条数据
        
            //为了批量插入,所以用的List集合
        List<OrderDetail> orderDetailList = new ArrayList<>();
        
            //这时就要看购物车中的数据了
        for (ShoppingCart cart : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail(); //订单明细
            BeanUtils.copyProperties(cart,orderDetail);
            orderDetail.setOrderId(orders.getId());  //关联的订单id,设置当前订单明细关联的订单id
            
            orderDetailList.add(orderDetail);   //通过遍历,可以把n条数据,都存放到集合中,用来批量插入
        }
        
        //批量插入n条数据
        orderDetailMapper.insertBatch(orderDetailList);  
            //它的批量插入,是直接放绕orderDetailMapper.xml中通过for-each标签实现的,没有在当前位置进行遍历
        
        //4.清空当前用户的购物车数据
            //因为当用户下单成功后,这个购物车中的数据就没用了
        shoppingCartMapper.deleteByUserId(userId);
        
        //5.封装VO返回结果 
        OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
                .id(orders.getId())
                .orderTime(orders.getOrderTime())
                .orderNumber(orders.getNumber())
                .orderAmount(orders.getAmount())
                .build();

        return orderSubmitVO;    
    }


    /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getById(userId);

        //调用微信支付接口，生成预支付交易单
        JSONObject jsonObject = weChatPayUtil.pay(
                ordersPaymentDTO.getOrderNumber(), //商户订单号
                new BigDecimal(0.01), //支付金额，单位 元
                "苍穹外卖订单", //商品描述
                user.getOpenid() //微信用户的openid
        );

        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }

        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));

        return vo;
    }

    /**
     * 支付成功，修改订单状态
     *
     * @param outTradeNo  这个就是订单号
     */
    public void paySuccess(String outTradeNo) {

        // 根据订单号查询订单
        Orders ordersDB = orderMapper.getByNumber(outTradeNo);

        // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.TO_BE_CONFIRMED)
                .payStatus(Orders.PAID)
                .checkoutTime(LocalDateTime.now())
                .build();

        orderMapper.update(orders);
        
        //通过webSocket向商家客户端/浏览器 推送消息  type  orderId content(内容)  这三个表示要传入的参数
        Map map = new HashMap();
        map.put("type",1);     //这个1表示来单提醒, 2表示的是客户催单 ;
            // 需要注意,这里是要把信息推送到浏览器客户端,所以这个type在后端没有定义,
                //type=1,而是浏览器客户端会解析出这代表的是"来单提醒",所以她就会处理这个逻辑,然后把提示信息放到弹窗上
                //我们就反过来,把浏览器客户端理解成服务器来处理我们发送的请求,就好理解了;
                // 我把后端代码翻遍了,也没找到type = 1 的定义,这回我理解了,后端根本就没定义,因为不是后端要解析
        map.put("orderId",ordersDB.getId());   //订单id
        map.put("content","订单号: " + outTradeNo);    //表示具体的提示内容,这里直接输出对应的订单号就可以了

        String json = JSON.toJSONString(map);//转换成json字符串
        //把上面这个封装好的字符串推送到商家页面
        webSocketServer.sendToAllClient(json);  
        //这个方法也是我们定义好的,,需要手动调用(即访问对应的这个路径,才会执行对应的方法的调用),不是启动主程序就自动调用的
            //感觉上面封装的推送消息,应该是前端要求我们传入的参数,虽然我们看不到,主要是我前端代码不是很熟悉;
            //还有上面的type=1,表示的是来单提醒,我也没找到哪里定义过,有可能是前端代码,可以把type=1解析成"来单提醒"
    }

    /**
     * 用户端订单分页查询
     * @param pageNum
     * @param pageSize
     * @param status     订单状态  1待付款  2待接单  3已接单  4派送中  5已完成  6已取消  
     * @return
     */
    @Override
    public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) {
        
        PageHelper.startPage(pageNum,pageSize);
        //分页查询还是要封装成一个PageQueryDTO对象,不能直接传上面参数; 因为你要用这个PageHelper工具,设置对应的页码和数量
            // 我看前面所有的分页查询,也都是封装成PageQueryDTO对象
        OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
        ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());   //这里自己没想到还要用用户id查
        ordersPageQueryDTO.setStatus(status);
        
        //1.分页条件查询
        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
        //需要注意,因为上面用了PageHelper插件,所以前面这里的返回结果+泛型必须是Page<实体类>  要遵守人家的规则

        //2. 查出订单明细,并封装成OrderVo进行响应  (自己也没想到还要查订单明细)
        List<OrderVO> list = new ArrayList();
        if (page != null && page.getTotal() > 0){
            //这里遍历实现,相当于就不用去mapper.xml中使用遍历标签了,同时也不用多表联查了,下面单表查了OrderDetail
            for (Orders orders : page) {

                Long orderId = orders.getId();  //订单id
                //查询订单详细表中的详细信息  (我还以为不用查详细表,就没去想,,为啥不想,因为懒)
                List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);

                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders,orderVO);
                orderVO.setOrderDetailList(orderDetails);
                
                list.add(orderVO);
            }
        }
        //因为最终我们要返回的是PageResult对象,而上面用的是PageHelper插件,返回的是Page,
            // 所以要想办法转换成PageResult对象;
            // 打开PageResult类,有long total(总记录数),List records(当前页数据集合)两个属性,
            // 所以当前如果能用page获取到这两个属性,就能封装成PageResult对象
        return new PageResult(page.getTotal(),list);
        //上面最后的list也是,直接传就可以,自己还以为要单独找到records的属性字段,就是表示当前页的数据集合
        //为啥传的是这两个属性,那就要看PageResult类中,是怎么定义的属性
        
        //补充,分页查询,看"java笔记_yue,第62点",
            // 这里其实OrderVO对象中private String orderDishes;还没有赋值
            //而且分页查询,没这么复杂以前分页查询,都不是这样实现的,当然这里复杂,那mapper.xml中就简单了,
            //相当于从多表查询变成了mapper.xml中的两个单表查询
    }

    /**
     * 查询订单详情
     * @param id  这个id指的是订单id
     * @return
     */
    @Override
    public OrderVO details(Long id) {
        //根据id查询订单
        Orders orders = orderMapper.getById(id);
        
        //查询该订单对应的菜品/套餐明细
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
            //测试,上面老师给的参考代码是 orderDetailMapper.getByOrderId(orders.getId),我感觉直接传id就行呀
            //测试完,一模一样,所以就用自己的代码了
        //log.info("自己传入的id: {}",id);
        //log.info("老师代码的getId: {}",orders.getId());
        
        //将该订单及其详情封装到OrderVo并返回
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(orders,orderVO);  
            //这里用的是属性拷贝,我自己还想把单个order对象转成字符串直接插入;我感觉应该也可以吧
                //下面是自己的想法,不行,查出来的订单时间,订单号码,配送地址,等等都是null,还是得上面属性拷贝
                //orderVO.setOrderDishes(orders.toString());
        orderVO.setOrderDetailList(orderDetailList);

        //小细节,我发现小程序页面对应的地址是null的,也很正常,因为orders和order_detail表中都没有详细的地址信息,
            //只有一个address_book_id,要单独去address_book才能查到详细的配送地址,
            // 主要是封装的orderVO也没有 配送地址这个属性
        return orderVO;
    }

    /**
     * 用户取消订单
     * @param id
     * @throws Exception
     */
    @Override
    public void userCancelById(Long id) throws Exception {
        
        //根据id查询订单
        Orders ordersDB = orderMapper.getById(id);
        // 校验订单是否存在
        if (ordersDB == null){
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
            //这里自己就没想到还要验证订单是否存在,自己直接就想调用update,更新订单状态为"已取消"
        }
        //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
        if (ordersDB.getStatus() > 2) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
            //1.这里老师代码就很清晰,,后面的状态都抛个异常就好了,,自己还一个状态一个状态去if来单独判断,不会变通
            //2. 业务要求是,接单状态和派送状态,,用户取消订单要电话沟通商家,,这里我就不知道怎么写代码了,,
                //然后老师这里代码就是直接抛出一个异常
        }
        Orders orders = new Orders();
        orders.setId(ordersDB.getId());

        // 订单处于待接单状态下取消，需要进行退款
        if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)){
            //调用微信支付退款接口
                //这里微信支付的调用退款,自己没用过,所以不知道调用哪个方法
            weChatPayUtil.refund(
                    ordersDB.getNumber(), //商户订单号
                    ordersDB.getNumber(), //商户退款单号
                    new BigDecimal(0.01),//退款金额，单位 元
                    new BigDecimal(0.01));//原订单金额

            //支付状态修改为 退款    (原来还有个支付状态,我以为只修改订单状态就够了)
            orders.setPayStatus(Orders.REFUND);
        }

        // 更新订单状态、取消原因、取消时间
        orders.setStatus(Orders.CANCELLED);  //这里我还以为只更新订单状态就够了
        orders.setCancelReason("用户取消");
        orders.setCancelTime(LocalDateTime.now());
        
        //最终更新订单状态为"订单已取消"
        orderMapper.update(orders);
    }

    /**
     * 再来一单
     * 再来一单的本质,就是把前面的菜品数据,再次添加到购物车;等再次下单的时候,会走最上面"下单接口方法"对应的逻辑
     * 这里老师新建的一个方法,扩充成了批量插入;   
     * 老师的想法是对的,要不我想直接在遍历的过程中调用insert语句,那遍历会频繁访问数据库,自己的想法不对
     * @param id
     */
    @Override
    public void repetition(Long id) {
        //目的:将菜品添加到购物车
        
        //购物车表和Order_detail表差不多,所以用Order_detail封装成ShoppingCart对象就可以了
            //需要注意,属性拷贝的时候,这个和DTO还不一样,DTO是完全对应的,而上面这属于两个表,id是不同的,这个不能拷贝
            //前面用拷贝的方法,基本都是DTO对象,然后缺的属性,单独set,而这里id不能拷贝过去
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);

        //下面需要用到userId
        Long userId = BaseContext.getCurrentId();
        
        List<ShoppingCart> shoppingCartList = new ArrayList();
        for (OrderDetail orderDetail : orderDetailList) {
            ShoppingCart shoppingCart = new ShoppingCart();
            BeanUtils.copyProperties(orderDetail,shoppingCart,"id");
                //需要注意,这个id两个表代表的不一样,所以不能拷贝,,后面加上,就表示忽视的意思,
                //以前能拷贝是因为是DTO,本身就是从一个表中补充的,所以可以拷贝对应所有属性,,这里需要注意
            
            shoppingCart.setUserId(userId);
            shoppingCart.setCreateTime(LocalDateTime.now());
            
            //为了批量插入,所以new了一个List集合,下面把每个shoppingCart对象放入到集合中
            shoppingCartList.add(shoppingCart);
        }
        
        //批量插入  (单独新建的一个方法)
        shoppingCartMapper.insertBatch(shoppingCartList);
    }

    /**
     * 商家管理端,订单查询,分页查询
     * @param ordersPageQueryDTO
     * @return
     */
    @Override
    public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        int pageNum = ordersPageQueryDTO.getPage();
        int pageSize = ordersPageQueryDTO.getPageSize();
        
        PageHelper.startPage(pageNum,pageSize);

        //调用mapper层对应的pageQuery分页查询方法
        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
        
        // 部分订单状态，需要额外返回订单菜品信息，将Orders转化为OrderVO
            //正常想法就是新建OrderVO实例,然后通过属性拷贝一点点实现,
                //但是下面直接调用了一个方法,,又没指定对象,所以应该是当前自定义的一个方法,专门封装了这个转换的过程
        List<OrderVO> orderVOList = getOrderVOList(page);
        
        long total = page.getTotal();
        //如果搜索内容为空,抛出异常,提示未找到相关订单
        return new PageResult(total,orderVOList);
        //需要注意这个PageResult里面属性名的定义,,所以才要传入对应的属性值

        //惊天发现,,下面代码直接传,在前端也能分页查出对应订单表中的数据
            //1. 测试的时候,把上面第389,393行代码注释掉就可以
            //2. 分析: 能直接实现分页查询的功能,
            // 那也就是说上面代码中的属性拷贝,,,包括orderDetailList封装到OrderVO,还有菜品orderDishes字符串的拼接,
            // 都可以不写代码,当然写了,逻辑非常清晰,代码肯定是没错误,
            // 但是前端就没有展示orderDishes这个菜品信息,那我完全没必要进行字符串拼接处理,让他null着就行
            //不仅如此,,那我连OrderVO的封装都不用封装呀,,,我真服了
        //return new PageResult(total,page.getResult());

    }

    /**
     * getOrderVOList方法的定义
     * 这个方法是自定义的,专门用于上面分页查询的时候,把Page<Orders>转换成List<OrderVO> orderVOList的过程
     * 下面注释的部分是自己的思路,不好,所以我直接用的老师的代码
     * @param page
     * @return
     */
    //private List<OrderVO> getOrderVOList(Page<Orders> page){
    //
    //   List<OrderVO> orderVOList = new ArrayList();
    //   
    //    if (page != null && page.size() > 0){
    //
    //        for (Orders orders : page) {
    //            OrderVO orderVO = new OrderVO();
    //            //将Orders通过属性拷贝放入到OrderVO里面的属性orderDishes中
    //            BeanUtils.copyProperties(orders,orderVO);
    //            
    //            Long ordersId = orders.getId();
    //            List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(ordersId);
    //            orderVO.setOrderDetailList(orderDetailList);
    //            
    //            //优化:前面的分页查询,代码都是这样写的,但是我们看OrderVO中,订单菜品信息,并不是直接是一个Orders的集合
    //                //而是一个String orderDishes,是String类型,所以按理说我们应该把Orders转成一个字符串
    //                //我明白了,,,我一直没理解对,,看"java笔记_yue,第62点"
    //            //补充:自定义一个方法,用来拼接菜品名称,即填充这个String orderDishes属性
    //            
    //            String orderDishes = getOrderDishesStr(orders);
    //            // 将订单菜品信息封装到orderVO中，并添加到orderVOList
    //            orderVO.setOrderDishes(orderDishes);
    //            
    //            orderVOList.add(orderVO);
    //        }
    //    }
    //    return orderVOList;
    //
    //    //补充,分页查询,看"java笔记_yue,第62点",
    //        // 这里其实OrderVO对象中private String orderDishes;还没有赋值
    //        //而且分页查询,没这么复杂以前分页查询,都不是这样实现的,当然这里复杂,那mapper.xml中就简单了,
    //        //相当于从多表查询变成了mapper.xml中的两个单表查询
    //}

    /**
     * getOrderVOList方法的定义
     * 老师的代码,我自己稍微改了一点儿
     * @param page
     * @return
     */
    private List<OrderVO> getOrderVOList(Page<Orders> page){ 
        
        List<OrderVO> orderVOList = new ArrayList<>();
        
        //1.下面这里就和上面自己的想法不一致了
            //我回顾前面的分页查询,都是直接return new PageResult(page.getTotal(),page.getResult());
            //所以这个page的getResult方法一直都有,但是自己给忘了
        //2.这个方法的作用,相当于把OrderVO中的List<OrderDetail> orderDetailList这个属性填充了,,
            //下面就不用自己填充了,看下面473行
        //3.但是,这里涉及到继承类Orders,所以下面每个Orders对象,都要单独通过遍历,对里面的属性拷贝,
            // 相当于Orders里面的id,name等等字段的赋值,因为OrderVO继承了Orders,所以它里面的属性和方法(公有)都可以用
        //4. 遍历代码的对比
            // 以前,自己不用这个page.getResult方法,就直接通过 for (Orders orders : page)遍历,,
            // 而当下代码是用的for (Orders orders : ordersList)来遍历,效果是一样的;  当然下面这种代码逻辑更清晰
        List<Orders> ordersList = page.getResult();

        if (page != null && page.size() > 0){

            for (Orders orders : ordersList) {
                // for (Orders orders : page)  这个是上面注释掉的代码,,我用来做对比
                
                //1.封装成OrderVO进行操作
                OrderVO orderVO = new OrderVO();
                
                //1.1 将每个orders中的属性,都封装到OrderVO对象中
                //将Orders通过属性拷贝放入到OrderVO里面的属性orderDishes中
                BeanUtils.copyProperties(orders,orderVO);
                
                //1.2  处理OrderVO对象中List<OrderDetail> orderDetailList这个属性
                    //1.2.1 正常我自己的想法是,对照上面注释掉的代码,415行
                        // 先通过订单id,去orderDetail表中查出对应的对象,然后在set,放入orderVO
                    //1.2.2 可是当下这里,并没有再去插入相关信息,也就是说上面page.getResult()这个方法,
                        //已经实现了这个功能
                
                //1.3 处理OrderVO类中的String orderDishes属性,,就是一个字符串的菜品信息,自己拼接下就可以
                    //感觉不处理,也不影响,不是关键字段
                    //前面的分页查询,代码都没有单独处理这个orderDishes属性,以前我误以为属性拷贝,就是在给这个属性赋值,
                    //其实并不是,,所以要单独对这个菜品信息的字符串进行拼接,我一直没理解,,可以看"java笔记_yue,第62点"
                String orderDishes = getOrderDishesStr(orders);
                //封装进OrderVO
                orderVO.setOrderDishes(orderDishes);
                
                //1.4 最终把封装好的orderVO放入到定义好的空List集合中
                orderVOList.add(orderVO);
            }
        }
        return orderVOList;
    }

    /**
     * 根据订单id获取菜品信息字符串
     *  下面的代码,我没有用老师提供的代码,老师的.stream().map() 是 Java 8 引入的一种更现代、更函数式的方式
     *      但是我没学过,不想了解,就没用
     * @param orders
     * @return 拼接后的菜品信息字符串
     */
    private String getOrderDishesStr(Orders orders) {
        // 查询订单菜品详情信息（订单中的菜品和数量）
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        // 创建一个 StringBuilder 来拼接字符串，这比使用字符串连接（+）更高效
        StringBuilder orderDishesStringBuilder = new StringBuilder();
            //厉害,,这里原来可以通过新建一个new StringBuilder()来拼接,以前这个讲过,都忘光了

        // 使用 for-each 循环遍历订单详情列表
        for (OrderDetail orderDetail : orderDetailList) {
            // 拼接菜品名称、数量和分号
            String orderDish = orderDetail.getName() + "*" + orderDetail.getNumber() + ";" + " ";
            // 将拼接后的字符串添加到 StringBuilder 中
            orderDishesStringBuilder.append(orderDish);
            
            //字符串的拼接,这里在orderDish后没有添加分隔符,也就会把所有的菜品信息,拼接到一起
                //最终的结果是:  馋嘴牛蛙*1; 炝炒圆白菜*1; 
        }

        // 将 StringBuilder 转换为字符串并返回
            //最终是把所有的菜品信息都拼接到了一起,并没有添加分隔符
        return orderDishesStringBuilder.toString();
    }

    /**
     * 各个状态的订单数量统计
     * @return
     */
    @Override
    public OrderStatisticsVO statistics() {
        
        //自己没想出来,想每个状态都单独通过遍历计数,感觉好麻烦,直接看示例代码了
        
        //根据状态,分别查询出待接单,待配送,派送中的订单数量
            //原来只需要这三个状态的订单数量,我看了下OrderStatisticsVO,里面只有这三个属性,表示订单数量
        Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);   //传入的常量表示  2待接单
        Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);   //传入的常量表示  3已接单
        Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);   //传入的常量表示  4派送中
        //关键是这个订单数量,怎么通过sql语句查询,通过状态查出有几个订单,然后通过类似.size大小应该就可以判断数量了
            //原来sql语句中,也可有专门计数的函数count,可以直接计数,我还以为底层要自己遍历呢
            //我又回顾了下mysql的 java基础笔记_yue,第1020;  mysql的确有很多函数,还有字符串的相关函数呢,,我都忘光了
        
        //将查询出的数据封装到OrderStatisticsVO中响应
        OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
        orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
        orderStatisticsVO.setConfirmed(confirmed);
        orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
        

        return orderStatisticsVO;
    }

    /**
     * 接单
     * @param ordersConfirmDTO
     */
    @Override
    public void confirm(OrdersConfirmDTO ordersConfirmDTO) {

        //因为下面的update方法要传入一个Orders对象,所以需要封装下
        Orders orders = Orders.builder()
                .id(ordersConfirmDTO.getId())
                .status(Orders.CONFIRMED)   //常量可以直接调用,,这里表示"3,是已接单的状态"
                .build();

        orderMapper.update(orders);
    }


    /**
     * 拒单
     * @param ordersRejectionDTO
     */
    @Override
    public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        //拒单是要考虑退款的

        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());
        // 订单只有存在且状态为2（待接单）才可以拒单
            //感觉这个逻辑有没有都行,商家前端页面,只有特定的待接单状态下才会有"拒单"的按钮,,
            // 所以即使我们不判断,商家也只能在"待接单状态下",才能点击"拒单"按钮
        if (!(ordersDB != null && ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED))){
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
            //1.示例代码给的逻辑是对的,,自己的错了,,所以我直接在自己条件上来个取反!符号,,就不按老师的代码写
                // 我的条件表示可以拒单,而这里应该是不能拒单的条件,然后才抛出异常
            //2.为啥后面不用"==",而是用的equals,你忘了吧,前面基础笔记讲过,
                // 对象的=号,表示判断是否是同一个对象,包括地址;而equals才表示的是判断里面的字符串的值是否相等
                //在Java中，== 运算符用于比较基本数据类型（如 int, char, boolean 等）的值，
                    // 或者用于比较两个对象引用是否指向同一个对象实例（即内存地址是否相同）,比如两个string对象
        }
        //支付状态
        Integer payStatus = ordersDB.getPayStatus();  //查看支付状态
        if (payStatus.equals(Orders.PAID)){  //此时的支付状态: 1 表示已付款
            //示例代码对比用的是==,这是不对的,Interger也算是对象,所以应该用equals来对比是否相等
                //我严重怀疑,前面的代码,在if中我都用的==来判断值的大小,可能都错了,
                // 只是文心一言说,范围在-128 到 127,所以==也可以判断,所以才没有报错,或者自己没注意过.
            
            //用户已付款状态下,,商家要退款
                //这个微信支付,自己总想不到,不会用
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
                //refund就是微信支付接口中,专门用于退款的方法;  这个方法会报异常,抛出异常到rejection方法上就行了
            
            log.info("申请退款：{}", refund);

        }
        
        //因为下面update方法,要传入orders对象,所以要封装一个
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.CANCELLED)   //订单状态: 6 表示已取消
                .rejectionReason(ordersRejectionDTO.getRejectionReason())  //拒绝原因
                    //这个拒绝原因,不是从ordersDB.getRejectionReason(),我说怎么获取不到;
                    //本来传进来的就是ordersRejectionDTO,只是前面通过id查询的ordersDB这个对象
                    //讨论这个问题,前面通过id查询ordersDB对象,获取到的是数据库中的信息,而后面再前端页面填写拒绝原因,
                        //此时数据库才更新,所以最新的拒绝原因ordersDB这个对象是获取不到的,
                .cancelTime(LocalDateTime.now())  
                    //取消时间,设置的当下;感觉直接getCancelTime也能获取,只不过是用户当时取消的时间,
                        // 所以当下是商家操作,还是应该设置当下时间; 
                .cancelReason(ordersRejectionDTO.getRejectionReason())  
                //因为前端页面,只显示了取消原因,所以这个传入的拒绝原因,就无法显示到页面;
                    // 因此,这里我就把拒绝原因传给这个cancelReason,这样就也能把"拒绝原因"显示到取消原因那列
                .build();
        
        //最后根据订单id更新订单状态、拒单原因、取消时间
        orderMapper.update(orders);
    }

    /**
     * 商家取消订单
     * @param ordersCancelDTO
     */
    @Override
    public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {

        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus.equals(Orders.PAID)) {
            //用户已支付，需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            
            log.info("申请退款：{}", refund);
        }

        // 管理端取消订单需要退款，根据订单id更新订单状态、取消原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersCancelDTO.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason(ordersCancelDTO.getCancelReason());
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
        
    }

    /**
     * 派送订单
     * @param id
     */
    @Override
    public void delivery(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在，并且状态为3
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为派送中
        orders.setStatus(Orders.DELIVERY_IN_PROGRESS);

        orderMapper.update(orders);
        
    }

    /**
     * 完成订单
     * @param id
     */
    @Override
    public void complete(Long id) {

        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在，并且状态为4
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为完成
        orders.setStatus(Orders.COMPLETED);
        orders.setDeliveryTime(LocalDateTime.now());

        orderMapper.update(orders);
    }


    /**
     * 检查客户的收货地址是否超出配送范围
     * 下面的代码,,我直接拷贝的,老师也没讲;类似一个小工具,用到直接调用
     * 将来如果要用到下面的代码开发,,可以直接问文心一言,代码有哪些不懂的,有哪些功能,怎么修改等等
     * @param address
     */
    private void checkOutOfRange(String address) {
        Map map = new HashMap();
        map.put("address",shopAddress);
        map.put("output","json");
        map.put("ak",ak);

        //获取店铺的经纬度坐标
        String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
            //doGet请求后的结果放到了userCoordinate字符串中,而传入的两个参数,表示这个doGet方法要求出入的参数
        
        JSONObject jsonObject = JSON.parseObject(shopCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            //"0" 通常表示请求成功,所以前面的!,不等号表示没有请求成功;  并不是表示status的值是null,我理解错了
            throw new OrderBusinessException("店铺地址解析失败");
        }

        //数据解析
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        String lat = location.getString("lat");
        String lng = location.getString("lng");
        //店铺经纬度坐标
        String shopLngLat = lat + "," + lng;

        map.put("address",address);
        //获取用户收货地址的经纬度坐标
        String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
        
        jsonObject = JSON.parseObject(userCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("收货地址解析失败");
        }

        //数据解析
        location = jsonObject.getJSONObject("result").getJSONObject("location");
        lat = location.getString("lat");
        lng = location.getString("lng");
        //用户收货地址经纬度坐标
        String userLngLat = lat + "," + lng;

        map.put("origin",shopLngLat);
        map.put("destination",userLngLat);
        map.put("steps_info","0");

        //路线规划
        String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);

        jsonObject = JSON.parseObject(json);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("配送路线规划失败");
        }

        //数据解析
        JSONObject result = jsonObject.getJSONObject("result");
        JSONArray jsonArray = (JSONArray) result.get("routes");
        Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");

        if(distance > 5000){
            //配送距离超过5000米
            throw new OrderBusinessException("超出配送范围");
        }
    }

    /**
     * 客户催单
     * @param id
     */
    @Override
    public void reminder(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);  //先查一下,这个订单是否存在

        // 校验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }
        
        //要给商家浏览器推送消息
            //下面封装需要传入的参数
        Map map = new HashMap();
        map.put("type",2);   //type=2,表示客户催单,前端客户端浏览器能够解析出来,并不是后端定义的; 1表示来单提醒
        map.put("orderId",id);   //订单id
        map.put("content","订单号: " + ordersDB.getNumber());   //content表示弹窗的提示内容信息
        
        //把上面的map转成json格式,才能传入下面的sendToAllClient方法
        String json = JSON.toJSONString(map);
        //通过websocket向客户端浏览器,,调用这个sendToAllClient方法,,就是推送消息
        webSocketServer.sendToAllClient(json);
        //1.自己总把前面学的spring task弄混,那个是定时处理,包括支付超时的订单;以及一直处于配送中的订单
        //2.而这里的websocket讲的是推送消息,长连接可以实现客户端和服务器互发消息,,
            // 这里主要就是后端服务器能给前端商家浏览器客户端发消息
    }
}
